<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>D3.js 仿 NotebookLM 思维导图</title>
    <style>
        body {
            font-family: "Google Sans", "Helvetica Neue", sans-serif;
            background-color: #1f1f1f;
            color: #e8eaed;
            margin: 0;
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }
        #mindmap-container {
            width: 100%;
            height: 100%;
        }
        .node {
            cursor: pointer;
            font-size: 14px;
        }
        .node rect {
            stroke-width: 1.5px;
            rx: 8;
            ry: 8;
        }
        .node .node-name {
            fill: #ffffff;
            text-anchor: start;
            pointer-events: none;
        }
        .expand-symbol {
            fill: #ffffff;
            font-size: 16px;
            font-weight: bold;
            text-anchor: middle;
            pointer-events: none;
        }
        .link {
            fill: none;
            stroke-opacity: 0.6;
            stroke-width: 2px;
        }
    </style>
</head>
<body>
    <svg id="mindmap-container"></svg>

    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script>
        // --- 配置项 ---
        // 你可以调整这个值来感受不同的动画节奏
        const DURATION = 400; 
        
        const mindmapData = {
            "name": "重来：更为简单有效的商业思维",
            "children": [
                { "name": "卸负篇 TAKEDOWNS", "children": [ { "name": "忘了现实世界" }, { "name": "哪来的从错误中学习" }, { "name": "计划即瞎猜" }, { "name": "何必壮大？" } ] },
                { "name": "行动篇 GO", "children": [ { "name": "在宇宙中留下你的足迹" }, { "name": "挠自己的痒处" }, { "name": "画沙为界，立场明确" }, { "name": "万不得已不筹资" } ] },
                { "name": "进阶篇 PROGRESS", "_children": [ { "name": "拥抱约束" }, { "name": "与其做个半成品，不如做个半产品" } ] },
                { "name": "效率篇 PRODUCTIVITY", "_children": [ { "name": "灵感是会过期的" }, { "name": "找到你的节奏" } ] },
                { "name": "对手篇 COMPETITORS", "_children": [ { "name": "不要抄袭" }, { "name": "要产品化，不要商品化" } ] },
                { "name": "文化篇 CULTURE", "_children": [] }
            ]
        };

        const width = window.innerWidth;
        const height = window.innerHeight;
        const margin = { top: 20, right: 120, bottom: 20, left: 160 };
        const nodeWidth = 280; // 默认节点宽度，将被动态计算覆盖
        const nodeHeight = 60;
        const maxNodeWidth = 360; // 节点最大宽度限制

        const svg = d3.select("#mindmap-container").attr("width", width).attr("height", height);
        const g = svg.append("g");
        const treeLayout = d3.tree().nodeSize([nodeHeight, nodeWidth]);
        const root = d3.hierarchy(mindmapData, d => d.children || d._children);
        root.x0 = height / 2;
        root.y0 = 0;
        
        // 计算节点宽度的函数
        function calculateNodeWidth(text) {
            // 创建临时文本元素来测量文本宽度
            const tempText = svg.append("text")
                .attr("class", "node-name")
                .style("visibility", "hidden")
                .text(text);
            
            // 获取文本宽度
            const textWidth = tempText.node().getComputedTextLength();
            tempText.remove(); // 移除临时元素
            
            // 返回基于文本宽度的节点宽度，最小150，最大maxNodeWidth
            return Math.min(Math.max(textWidth + 30, 150), maxNodeWidth);
        }
        
        // 为每个节点计算并存储其宽度
        root.descendants().forEach((d, i) => {
            d.id = i;
            d.nodeWidth = calculateNodeWidth(d.data.name);
            if (d.depth > 1 && d.children) {
                d._children = d.children;
                d.children = null;
            }
        });

        const zoom = d3.zoom().scaleExtent([0.1, 3]).on("zoom", (event) => g.attr("transform", event.transform));
        svg.call(zoom);
        const initialTransform = d3.zoomIdentity.translate(margin.left, height / 2).scale(0.9);
        svg.call(zoom.transform, initialTransform);

        function update(source) {
            const treeData = treeLayout(root);
            const nodes = treeData.descendants();
            const links = treeData.links();

            // 修改这一行来增加层级之间的水平距离
            nodes.forEach(d => { d.y = d.depth * nodeWidth * 1.5; }); // 将原来的倍数从1改为1.5倍

            const node = g.selectAll("g.node").data(nodes, d => d.id);
            const nodeEnter = node.enter().append("g")
                .attr("class", "node")
                .attr("transform", d => `translate(${source.y0},${source.x0})`)
                .on("click", (event, d) => { toggle(d); update(d); });

            // 使用每个节点的计算宽度
            nodeEnter.append("rect").attr("y", -20).attr("height", 40).attr("width", 0)
                .style("fill", d => d._children ? "#4E5166" : "#2E403B")
                .style("stroke", d => d._children ? "#4E5166" : "#424B54");
                
            nodeEnter.append("text").attr("class", "node-name").attr("dx", 15).attr("dy", 4).text(d => d.data.name).style("fill-opacity", 0);
            
            nodeEnter.append("circle").attr("r", 10).attr("class", "expand-button")
                .style("stroke", "#ffffff")
                .style("stroke-width", "1.5px")
                .style("fill", d => d._children ? "#4E5166" : "#2E403B");
                
            nodeEnter.append("text").attr("class", "expand-symbol").attr("dy", "0.35em").text(d => d._children ? ">" : (d.children ? "<" : ""));

            const nodeUpdate = nodeEnter.merge(node);
            const nodeUpdateTransition = nodeUpdate.transition()
                .duration(DURATION)
                .ease(d3.easeCubicOut); // 应用"缓出"效果，实现由快到慢的减速感

            nodeUpdateTransition.attr("transform", d => `translate(${d.y},${d.x})`);
            
            // 使用每个节点的计算宽度
            nodeUpdateTransition.select("rect")
                .attr("width", d => d.nodeWidth)
                .style("fill", d => d._children ? "#4E5166" : "#2E403B")
                .style("stroke", d => d._children ? "#4E5166" : "#424B54");
                
            nodeUpdateTransition.select("circle")
                .attr("cx", d => hasChildren(d) ? d.nodeWidth : 0)
                .style("display", d => hasChildren(d) ? "block" : "none")
                .style("fill", d => d._children ? "#4E5166" : "#2E403B");
                
            nodeUpdateTransition.select(".expand-symbol")
                .attr("x", d => hasChildren(d) ? d.nodeWidth : 0)
                .style("display", d => hasChildren(d) ? "block" : "none")
                .text(d => d._children ? ">" : (d.children ? "<" : ""));
                
            nodeUpdateTransition.select(".node-name").style("fill-opacity", 1);
            
            const nodeExitTransition = node.exit().transition()
                .duration(DURATION)
                .ease(d3.easeCubicOut) // 收起时也应用"缓出"减速感
                .attr("transform", d => `translate(${source.y},${source.x})`)
                .remove();
            
            nodeExitTransition.select("rect").attr("width", 0);
            nodeExitTransition.select("text").style("fill-opacity", 0);

            const link = g.selectAll("path.link").data(links, d => d.target.id);
            const linkEnter = link.enter().insert("path", "g")
                .attr("class", "link")
                .attr("d", d => { 
                    const o = { x: source.x0, y: source.y0 }; 
                    return elbow({ source: o, target: o }); 
                })
                .style("stroke", d => d.target.data.children || d.target.data._children ? "#4E5166" : "#424B54");
            
            link.merge(linkEnter).transition()
                .duration(DURATION)
                .ease(d3.easeCubicOut) // 连线动画也应用"缓出"减速感
                .attr("d", elbow)
                .style("stroke", d => d.target.data.children || d.target.data._children ? "#4E5166" : "#424B54");

            link.exit().transition()
                .duration(DURATION)
                .ease(d3.easeCubicOut) // 连线消失时也应用"缓出"减速感
                .attr("d", d => { 
                    const o = { x: source.x, y: source.y }; 
                    return elbow({ source: o, target: o }); 
                })
                .remove();

            nodes.forEach(d => { d.x0 = d.x; d.y0 = d.y; });
        }
        
        function toggle(d) { if (d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; } }
        function hasChildren(d) { return d.children || d._children; }
        
        // 修改elbow函数使用动态节点宽度
        function elbow(d) { 
            const sX = d.source.x;
            const sY = d.source.y + (hasChildren(d.source) ? d.source.nodeWidth : 0);
            const tX = d.target.x;
            const tY = d.target.y; 
            return `M ${sY},${sX}C ${(sY + tY) / 2},${sX} ${(sY + tY) / 2},${tX} ${tY},${tX}`; 
        }

        update(root);
    </script>
</body>
</html>
